/*
 * jPOS Project [http://jpos.org]
 * Copyright (C) 2000-2015 Alejandro P. Revilla
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.jpos.util;

import org.jpos.core.Configuration;
import org.jpos.core.ConfigurationException;

import java.io.*;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * Rotates log daily and compress the previous log.
 *
 * @author <a href="mailto:alcarraz@jpos.org">Andr&eacute;s Alcarraz</a>
 * @since jPOS 1.5.1
 */
public class DailyLogListener extends RotateLogListener {
	private static final String DEF_SUFFIX = ".log";
	private static final int DEF_WIN = 24 * 3600;
	private static final long DEF_MAXSIZE = -1;
	private static final String DEF_DATE_FMT = "-yyyy-MM-dd";
	private static final int NONE = 0;
	private static final int GZIP = 1;
	private static final int ZIP = 2;
	private static final int DEF_COMPRESSION = NONE;
	private static final int DEF_BUFFER_SIZE = 128 * 1024;//128 KB
	private static final String[] DEF_COMPRESSED_SUFFIX = {"", ".gz", ".zip"};
	private static final Map<String, Integer> COMPRESSION_FORMATS = new HashMap<String, Integer>(3);

	static {
		COMPRESSION_FORMATS.put("none", NONE);
		COMPRESSION_FORMATS.put("gzip", GZIP);
		COMPRESSION_FORMATS.put("zip", ZIP);
	}

	/**
	 * Creates a new instance of DailyLogListener
	 */
	public DailyLogListener() {
		setLastDate(getDateFmt().format(new Date()));
	}

	public void setConfiguration(Configuration cfg) throws ConfigurationException {
		String suffix = cfg.get("suffix", DEF_SUFFIX), prefix = cfg.get("prefix");
		setSuffix(suffix);
		setPrefix(prefix);
		Integer formatObj =
				COMPRESSION_FORMATS
						.get(cfg.get("compression-format", "none").toLowerCase());
		int compressionFormat = formatObj == null ? 0 : formatObj;
		setCompressionFormat(compressionFormat);
		setCompressedSuffix(cfg.get("compressed-suffix",
				DEF_COMPRESSED_SUFFIX[compressionFormat]));
		setCompressionBufferSize(cfg.getInt("compression-buffer-size",
				DEF_BUFFER_SIZE));
		logName = prefix + suffix;
		maxSize = cfg.getLong("maxsize", DEF_MAXSIZE);
		try {
			openLogFile();
		} catch (IOException e) {
			throw new ConfigurationException("error opening file: " + logName,
					e);
		}
		sleepTime = cfg.getInt("window", DEF_WIN);
		if (sleepTime <= 0)
			sleepTime = DEF_WIN;
		sleepTime *= 1000;
		DateFormat fmt = new SimpleDateFormat(cfg.get("date-format", DEF_DATE_FMT));

		setDateFmt(fmt);
		setLastDate(fmt.format(new Date()));
		Date time;
		try {
			time = new SimpleDateFormat("HH:mm:ss").parse(cfg.get("first-rotate-time", "00:00:00"));
		} catch (ParseException ex) {
			throw new ConfigurationException("Bad 'first-rotate-time' format " +
					"expected HH(0-23):mm:ss ", ex);
		}
		String strDate = cfg.get("first-rotate-date", null);
		//calculate the first execution time
		Calendar cal = Calendar.getInstance();
		cal.set(Calendar.MILLISECOND, 0);
		Calendar calTemp = Calendar.getInstance();
		calTemp.setTime(time);
		cal.set(Calendar.SECOND, calTemp.get(Calendar.SECOND));
		cal.set(Calendar.MINUTE, calTemp.get(Calendar.MINUTE));
		cal.set(Calendar.HOUR_OF_DAY, calTemp.get(Calendar.HOUR_OF_DAY));

		if (strDate != null) {
			Date date;
			try {
				date = new SimpleDateFormat("yyyy-MM-dd").parse(strDate);
			} catch (ParseException ex) {
				throw new ConfigurationException("Bad 'first-rotate-date' " +
						"format, expected (yyyy-MM-dd)", ex);
			}
			calTemp.setTime(date);
			cal.set(calTemp.get(Calendar.YEAR), calTemp.get(Calendar.MONTH),
					calTemp.get(Calendar.DATE));
		}
		//here cal contains the first execution, let/s calculate the next one
		calTemp.setTime(new Date());
		//if first executiontime already happened
		if (cal.before(calTemp)) {
			//how many windows between cal and now
			long n = (calTemp.getTimeInMillis() - cal.getTimeInMillis()) /
					sleepTime;
			cal.setTimeInMillis(cal.getTimeInMillis() + sleepTime * (n + 1));
		}
		DefaultTimer.getTimer().scheduleAtFixedRate(
				rotate = new DailyRotate(), cal.getTime(), sleepTime);
	}

	public synchronized void logRotate() throws IOException {
		closeLogFile();
		super.close();
		setPrintStream(null);
		String suffix = getSuffix() + getCompressedSuffix();
		String newName = getPrefix() + getLastDate();
		int i = 0;
		File dest = new File(newName + suffix), source = new File(logName);
		while (dest.exists())
			dest = new File(newName + "." + ++i + suffix);
		source.renameTo(dest);
		setLastDate(getDateFmt().format(new Date()));
		openLogFile();
		compress(dest);
	}

	/**
	 * Holds value of property suffix.
	 */
	private String suffix = DEF_SUFFIX;

	/**
	 * Getter for property suffix.
	 *
	 * @return Value of property suffix.
	 */
	public String getSuffix() {
		return this.suffix;
	}

	/**
	 * Setter for property suffix.
	 *
	 * @param suffix New value of property suffix.
	 */
	public void setSuffix(String suffix) {
		this.suffix = suffix;
	}

	/**
	 * Holds value of property prefix.
	 */
	private String prefix;

	/**
	 * Getter for property prefix.
	 *
	 * @return Value of property prefix.
	 */
	public String getPrefix() {
		return this.prefix;
	}

	/**
	 * Setter for property prefix.
	 *
	 * @param prefix New value of property prefix.
	 */
	public void setPrefix(String prefix) {
		this.prefix = prefix;
	}

	/**
	 * Holds value of property rotateCount.
	 */
	private int rotateCount;

	/**
	 * Getter for property rotateCount.
	 *
	 * @return Value of property rotateCount.
	 */
	public int getRotateCount() {
		return this.rotateCount;
	}

	/**
	 * Setter for property rotateCount.
	 *
	 * @param rotateCount New value of property rotateCount.
	 */
	public void setRotateCount(int rotateCount) {
		this.rotateCount = rotateCount;
	}

	/**
	 * Holds value of property dateFmt.
	 */
	private DateFormat dateFmt = new SimpleDateFormat(DEF_DATE_FMT);

	/**
	 * Getter for property dateFmt.
	 *
	 * @return Value of property dateFmt.
	 */
	public DateFormat getDateFmt() {
		return this.dateFmt;
	}

	/**
	 * Setter for property dateFmt.
	 *
	 * @param dateFmt New value of property dateFmt.
	 */
	public void setDateFmt(DateFormat dateFmt) {
		this.dateFmt = dateFmt;
	}

	/**
	 * Holds value of property lastDate.
	 */
	private String lastDate;

	/**
	 * Getter for property lastDate.
	 *
	 * @return Value of property lastDate.
	 */
	public String getLastDate() {
		return this.lastDate;
	}

	/**
	 * Setter for property lastDate.
	 *
	 * @param lastDate New value of property lastDate.
	 */
	public void setLastDate(String lastDate) {
		this.lastDate = lastDate;
	}

	/**
	 * Holds value of property compressedSuffix.
	 */
	private String compressedSuffix = DEF_COMPRESSED_SUFFIX[DEF_COMPRESSION];

	/**
	 * Getter for property compressedExt.
	 *
	 * @return Value of property compressedExt.
	 */
	public String getCompressedSuffix() {
		return this.compressedSuffix;
	}

	/**
	 * Setter for property compressedExt.
	 *
	 * @param compressedSuffix New value of property compressedExt.
	 */
	public void setCompressedSuffix(String compressedSuffix) {
		this.compressedSuffix = compressedSuffix;
	}

	/**
	 * Hook method that creates a thread to compress the file f.
	 *
	 * @param f the file name
	 * @return a thread to compress the file and null if it is not necesary
	 */
	protected Thread getCompressorThread(File f) {
		return new Thread(new Compressor(f), "DailyLogListener-Compressor");
	}

	/**
	 * Hook method that creates an output stream that will compress the data.
	 *
	 * @param f the file name
	 * @return ZIP/GZip OutputStream
	 * @throws java.io.IOException on error
	 */
	protected OutputStream getCompressedOutputStream(File f) throws IOException {
		OutputStream os = new BufferedOutputStream(new FileOutputStream(f));
		if (getCompressionFormat() == ZIP) {
			ZipOutputStream ret = new ZipOutputStream(os);
			ret.putNextEntry(new ZipEntry(logName));
			return ret;
		} else {
			return new GZIPOutputStream(os);
		}
	}

	protected void closeCompressedOutputStream(OutputStream os) throws IOException {
		if (os instanceof DeflaterOutputStream)
			((DeflaterOutputStream) os).finish();
		os.close();
	}

	protected void logDebugEx(String msg, Throwable e) {
		ByteArrayOutputStream os = new ByteArrayOutputStream();
		PrintStream ps = new PrintStream(os);
		ps.println(msg);
		e.printStackTrace(ps);
		ps.close();
		logDebug(os.toString());

	}

	protected class Compressor implements Runnable {


		File f;

		public Compressor(File f) {
			this.f = f;
		}

		public void run() {
			OutputStream os = null;
			InputStream is = null;
			File tmp = null;
			try {
				tmp = File.createTempFile(f.getName(), ".tmp", f.getParentFile());
				os = getCompressedOutputStream(tmp);
				is = new BufferedInputStream(new FileInputStream(f));
				byte[] buff = new byte[getCompressionBufferSize()];
				int read;
				do {
					read = is.read(buff);
					if (read > 0)
						os.write(buff, 0, read);
				} while (read > 0);

			} catch (Throwable ex) {
				logDebugEx("error compressing file " + f, ex);
			} finally {
				try {
					if (is != null)
						is.close();
					if (os != null)
						closeCompressedOutputStream(os);
					if (f != null) {
						f.delete();
						if (tmp != null)
							tmp.renameTo(f);
					}
				} catch (Throwable ex) {
					logDebugEx("error closing files", ex);
				}

			}

		}

	}

	/**
	 * Holds value of property compressionFormat.
	 */
	private int compressionFormat = DEF_COMPRESSION;

	/**
	 * Getter for property compressionFormat.
	 *
	 * @return Value of property compressionFormat.
	 */
	public int getCompressionFormat() {
		return this.compressionFormat;
	}

	/**
	 * Setter for property compressionFormat.
	 *
	 * @param compressionFormat New value of property compressionFormat.
	 */
	public void setCompressionFormat(int compressionFormat) {
		this.compressionFormat = compressionFormat;
	}

	/**
	 * Holds value of property compressionBufferSize.
	 */
	private int compressionBufferSize = DEF_BUFFER_SIZE;

	/**
	 * Getter for property compressionBufferSize.
	 *
	 * @return Value of property compressionBufferSize.
	 */
	public int getCompressionBufferSize() {
		return this.compressionBufferSize;
	}

	/**
	 * Setter for property compressionBufferSize.
	 *
	 * @param compressionBufferSize New value of property compressionBufferSize.
	 */
	public void setCompressionBufferSize(int compressionBufferSize) {
		this.compressionBufferSize = compressionBufferSize >= 0 ?
				compressionBufferSize : DEF_BUFFER_SIZE;
	}

	protected void checkSize() {
		if (maxSize > 0)
			super.checkSize();
	}

	/**
	 * Hook method to optionally compress the file
	 *
	 * @param logFile the file name
	 */
	protected void compress(File logFile) {
		if (getCompressionFormat() != NONE) {
			Thread t = getCompressorThread(logFile);
			try {
				if (t != null)
					t.start();
			} catch (Exception e) {
				logDebugEx("error compressing file", e);
			}
		}
	}

	final class DailyRotate extends Rotate {
		public void run() {
			super.run();
			setLastDate(getDateFmt().format(new Date(scheduledExecutionTime())));
		}

	}
}
